package cn.dansj.common.mail.utils.javamail;

import cn.dansj.common.mail.utils.*;
import cn.dansj.common.utils.transfer.AssertUtils;
import javax.activation.FileTypeMap;
import javax.mail.*;
import javax.mail.internet.MimeMessage;

import java.io.InputStream;
import java.util.*;

public class JavaMailSenderImpl implements JavaMailSender {

    public static final String DEFAULT_PROTOCOL = "smtp";

    public static final int DEFAULT_PORT = -1;

    private static final String HEADER_MESSAGE_ID = "Message-ID";

    private Properties javaMailProperties = new Properties();

    private Session session;

    private String protocol;

    private String host;

    private int port = DEFAULT_PORT;

    private String username;

    private String password;

    private String defaultEncoding;

    private FileTypeMap defaultFileTypeMap;

    public JavaMailSenderImpl() {
        ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
        fileTypeMap.afterPropertiesSet();
        this.defaultFileTypeMap = fileTypeMap;
    }

    public void setJavaMailProperties(Properties javaMailProperties) {
        this.javaMailProperties = javaMailProperties;
        synchronized (this) {
            this.session = null;
        }
    }

    public Properties getJavaMailProperties() {
        return this.javaMailProperties;
    }

    public synchronized void setSession(Session session) {
        AssertUtils.notNull(session, "Session must not be null");
        this.session = session;
    }

    public synchronized Session getSession() {
        if (this.session == null) {
            this.session = Session.getInstance(this.javaMailProperties);
        }
        return this.session;
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public String getProtocol() {
        return this.protocol;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getHost() {
        return this.host;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public int getPort() {
        return this.port;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return this.username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPassword() {
        return this.password;
    }

    public void setDefaultEncoding(String defaultEncoding) {
        this.defaultEncoding = defaultEncoding;
    }

    public String getDefaultEncoding() {
        return this.defaultEncoding;
    }

    public void setDefaultFileTypeMap(FileTypeMap defaultFileTypeMap) {
        this.defaultFileTypeMap = defaultFileTypeMap;
    }

    public FileTypeMap getDefaultFileTypeMap() {
        return this.defaultFileTypeMap;
    }

    @Override
    public void send(SimpleMailMessage... simpleMessages) throws MailException {
        List<MimeMessage> mimeMessages = new ArrayList<>(simpleMessages.length);
        for (SimpleMailMessage simpleMessage : simpleMessages) {
            MimeMailMessage message = new MimeMailMessage(createMimeMessage());
            simpleMessage.copyTo(message);
            mimeMessages.add(message.getMimeMessage());
        }
        doSend(mimeMessages.toArray(new MimeMessage[0]), simpleMessages);
    }

    @Override
    public MimeMessage createMimeMessage() {
        return new SmartMimeMessage(getSession(), getDefaultEncoding(), getDefaultFileTypeMap());
    }

    @Override
    public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
        try {
            return new MimeMessage(getSession(), contentStream);
        } catch (Exception ex) {
            throw new MailParseException("Could not parse raw MIME content", ex);
        }
    }

    @Override
    public void send(MimeMessage... mimeMessages) throws MailException {
        doSend(mimeMessages, null);
    }

    protected void doSend(MimeMessage[] mimeMessages, Object[] originalMessages) throws MailException {
        Map<Object, Exception> failedMessages = new LinkedHashMap<>();
        Transport transport = null;
        try {
            for (int i = 0; i < mimeMessages.length; i++) {

                // Check transport connection first...
                if (transport == null || !transport.isConnected()) {
                    if (transport != null) {
                        try {
                            transport.close();
                        } catch (Exception ex) {
                            // Ignore - we're reconnecting anyway
                        }
                        transport = null;
                    }
                    try {
                        transport = connectTransport();
                    } catch (AuthenticationFailedException ex) {
                        throw new MailAuthenticationException(ex);
                    } catch (Exception ex) {
                        // Effectively, all remaining messages failed...
                        for (int j = i; j < mimeMessages.length; j++) {
                            Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]);
                            failedMessages.put(original, ex);
                        }
                        throw new MailSendException("Mail server connection failed", ex, failedMessages);
                    }
                }

                // Send message via current transport...
                MimeMessage mimeMessage = mimeMessages[i];
                try {
                    if (mimeMessage.getSentDate() == null) {
                        mimeMessage.setSentDate(new Date());
                    }
                    String messageId = mimeMessage.getMessageID();
                    mimeMessage.saveChanges();
                    if (messageId != null) {
                        // Preserve explicitly specified message id...
                        mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
                    }
                    Address[] addresses = mimeMessage.getAllRecipients();
                    transport.sendMessage(mimeMessage, (addresses != null ? addresses : new Address[0]));
                } catch (Exception ex) {
                    Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
                    failedMessages.put(original, ex);
                }
            }
        } finally {
            try {
                if (transport != null) {
                    transport.close();
                }
            } catch (Exception ex) {
                if (!failedMessages.isEmpty()) {
                    throw new MailSendException("Failed to close server connection after message failures", ex, failedMessages);
                } else {
                    throw new MailSendException("Failed to close server connection after message sending", ex);
                }
            }
        }

        if (!failedMessages.isEmpty()) {
            throw new MailSendException(failedMessages);
        }
    }

    protected Transport connectTransport() throws MessagingException {
        String username = getUsername();
        String password = getPassword();
        if ("".equals(username)) {  // probably from a placeholder
            username = null;
            if ("".equals(password)) {  // in conjunction with "" username, this means no password to use
                password = null;
            }
        }

        Transport transport = getTransport(getSession());
        transport.connect(getHost(), getPort(), username, password);
        return transport;
    }

    protected Transport getTransport(Session session) throws NoSuchProviderException {
        String protocol = getProtocol();
        if (protocol == null) {
            protocol = session.getProperty("mail.transport.protocol");
            if (protocol == null) {
                protocol = DEFAULT_PROTOCOL;
            }
        }
        return session.getTransport(protocol);
    }
}
